Rustでライフゲームを作ってみた
ゲームソリューション部の えがわ です。
今回はRustをとりあえず触ってみる目的でライフゲームを作成してみました。
ライフゲームとは?
グリッド状のマス目にセル(細胞)を配置し、いくつかの簡単なルールに従って世代を繰り返していきます。
セルは「生きている」か「死んでいる」かの2状態があり、周囲のセルに影響されながら新たな形を生み出します。
見るだけで楽しいアニメーションが作れたり、予測不能なパターンが生まれるので、プログラミング勉強での面白い題材となっています。
完成系
ライフゲームのルール
ライフゲームはルールが決まっています。
- 誕生:死んでいるセルの周囲にちょうど3つの生きたセルがあれば、そのセルは次の世代で「生」へと変わります。
- 生存:生きているセルの周囲に2つまたは3つの生きたセルがあれば、そのセルは次の世代でも「生」のままです。
- 過疎:生きているセルの周囲に1つ以下の生きたセルしかない場合、そのセルは次の世代で「死」に変わります。これは過疎による死です。
- 過密:生きているセルの周囲に4つ以上の生きたセルがある場合、そのセルは次の世代で「死」に変わります。これは過密による死です。
環境
- Ubuntu 22.04.4 LTS(WSL2)
- cargo 1.82.0
- rustc 1.82.0
環境構築
Rustを実行するための環境構築を行います。
Rustのインストール
rustup を使用してRustをインストールします。
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
プロンプトが表示されたら、default のオプション(1 番)を選択してRustをインストールします。
.cargo/bin が PATH に追加されるので、インストールが適切に行われたか確認します。
source $HOME/.cargo/env
rustc --version
正常にインストールされていれば、Rustコンパイラのバージョンが表示されます。
プロジェクトの作成
以下のコマンドでプロジェクトを作成、そして初期プロジェクトを起動してみます。
cargo new lifegame_rust
cd lifegame_rust/
cargo build
cargo run
Hello, world!
が表示されれば環境構築は完了です。
ライフゲームを作成
プログラムの全貌はこちら
use std::time::Duration;
use std::{thread, io};
const WIDTH: usize = 80;
const HEIGHT: usize = 40;
fn main() {
let mut grid = vec![vec![false; WIDTH]; HEIGHT];
// Glider
grid[1][2] = true;
grid[2][3] = true;
grid[3][1] = true;
grid[3][2] = true;
grid[3][3] = true;
// Glider Gun
let glider_gun_coords = [
(5, 1), (5, 2), (6, 1), (6, 2), (3, 13), (3, 14), (4, 12), (4, 16), (5, 11), (5, 17),
(6, 11), (6, 15), (6, 17), (6, 18), (7, 11), (7, 17), (8, 12), (8, 16), (9, 13), (9, 14),
(1, 25), (2, 23), (2, 25), (3, 21), (3, 22), (4, 21), (4, 22), (5, 21), (5, 22), (6, 23),
(6, 25), (7, 25), (3, 35), (3, 36), (4, 35), (4, 36),
];
for &(x, y) in &glider_gun_coords {
grid[x][y] = true;
}
// LWSS
let lwss_coords = [
(20, 20), (21, 20), (22, 20), (23, 20), (20, 23), (23, 23), (23, 21), (23, 22), (21, 24),
(22, 24)
];
for &(x, y) in &lwss_coords {
grid[y][x] = true;
}
// Pulsar
let pulsar_coords = [
(25, 25), (26, 25), (27, 25), (33, 25), (34, 25), (35, 25),
(23, 27), (28, 27), (30, 27), (22, 27), (31, 27), (23, 28),
(28, 28), (30, 28), (22, 28), (31, 28), (22, 29), (31, 29),
(22, 30), (31, 30), (22, 32), (23, 32), (28, 32), (30, 32),
(31, 32), (22, 33), (23, 33), (28, 33), (30, 33), (31, 33),
(25, 35), (26, 35), (27, 35), (33, 35), (34, 35), (35, 35),
];
for &(x, y) in &pulsar_coords {
grid[y][x] = true;
}
loop {
print_grid(&grid);
grid = next_generation(&grid);
thread::sleep(Duration::from_millis(100));
clear_screen();
}
}
fn next_generation(grid: &Vec<Vec<bool>>) -> Vec<Vec<bool>> {
let mut new_grid = vec![vec![false; WIDTH]; HEIGHT];
for y in 0..HEIGHT {
for x in 0..WIDTH {
let alive_neighbors = count_alive_neighbors(grid, x, y);
if grid[y][x] {
// 生きているセル
if alive_neighbors == 2 || alive_neighbors == 3 {
new_grid[y][x] = true;
}
} else {
// 死んでいるセル
if alive_neighbors == 3 {
new_grid[y][x] = true;
}
}
}
}
new_grid
}
fn count_alive_neighbors(grid: &Vec<Vec<bool>>, x: usize, y: usize) -> usize {
let mut count = 0;
for dy in -1..=1 {
for dx in -1..=1 {
if dx == 0 && dy == 0 {
continue;
}
let nx = (x as isize + dx) as usize;
let ny = (y as isize + dy) as usize;
if nx < WIDTH && ny < HEIGHT && grid[ny][nx] {
count += 1;
}
}
}
count
}
fn print_grid(grid: &Vec<Vec<bool>>) {
let mut output = String::new();
for row in grid {
for &cell in row {
if cell {
output.push('■');
} else {
output.push(' ');
}
}
output.push('\n');
}
println!("{}", output);
}
fn clear_screen() {
print!("{}[2J", 27 as char);
print!("{}[1;1H", 27 as char);
}
初期配置
ライフゲームにはいくつかの有名な初期配置があり、それぞれに名前が付けられています。
グライダー
45度の角度で対角線上に移動する小さなパターン
// Glider
grid[1][2] = true;
grid[2][3] = true;
grid[3][1] = true;
grid[3][2] = true;
grid[3][3] = true;
グライダーガン
定期的にグライダーを生成するパターン
// Glider Gun
let glider_gun_coords = [
(5, 1), (5, 2), (6, 1), (6, 2), (3, 13), (3, 14), (4, 12), (4, 16), (5, 11), (5, 17),
(6, 11), (6, 15), (6, 17), (6, 18), (7, 11), (7, 17), (8, 12), (8, 16), (9, 13), (9, 14),
(1, 25), (2, 23), (2, 25), (3, 21), (3, 22), (4, 21), (4, 22), (5, 21), (5, 22), (6, 23),
(6, 25), (7, 25), (3, 35), (3, 36), (4, 35), (4, 36),
];
ライトウェイトスペースシップ (Light-weight spaceship, LWSS)
4x5のセルで構成される移動パターン
// LWSS
let lwss_coords = [
(20, 20), (21, 20), (22, 20), (23, 20), (20, 23), (23, 23), (23, 21), (23, 22), (21, 24),
(22, 24)
];
パルサー (Pulsar)
3x3ブロックが4つの位置にあり、その間を3x1ブロックが充填されています。
// Pulsar
let pulsar_coords = [
(25, 25), (26, 25), (27, 25), (33, 25), (34, 25), (35, 25),
(23, 27), (28, 27), (30, 27), (22, 27), (31, 27), (23, 28),
(28, 28), (30, 28), (22, 28), (31, 28), (22, 29), (31, 29),
(22, 30), (31, 30), (22, 32), (23, 32), (28, 32), (30, 32),
(31, 32), (22, 33), (23, 33), (28, 33), (30, 33), (31, 33),
(25, 35), (26, 35), (27, 35), (33, 35), (34, 35), (35, 35),
];
処理詳細
周囲のセル数を取得
セルの周囲の生きているセル数を取得しています。
fn count_alive_neighbors(grid: &Vec<Vec<bool>>, x: usize, y: usize) -> usize {
let mut count = 0;
for dy in -1..=1 {
for dx in -1..=1 {
if dx == 0 && dy == 0 {
continue;
}
let nx = (x as isize + dx) as usize;
let ny = (y as isize + dy) as usize;
if nx < WIDTH && ny < HEIGHT && grid[ny][nx] {
count += 1;
}
}
}
count
}
次の世代を生成
次の世代を生成するためにGridを再生成しています。
誕生 or 生存セルのみtrueを代入しています。
fn next_generation(grid: &Vec<Vec<bool>>) -> Vec<Vec<bool>> {
let mut new_grid = vec![vec![false; WIDTH]; HEIGHT];
for y in 0..HEIGHT {
for x in 0..WIDTH {
let alive_neighbors = count_alive_neighbors(grid, x, y);
if grid[y][x] {
// 生きているセル
if alive_neighbors == 2 || alive_neighbors == 3 {
new_grid[y][x] = true;
}
} else {
// 死んでいるセル
if alive_neighbors == 3 {
new_grid[y][x] = true;
}
}
}
}
new_grid
}
さいごに
Rustを使用してライフゲームを作成してみました。
ライフゲームはコンソール画面で楽しむことができるので、プログラムの勉強として、そして、ゲーム制作のとっかかりとして、とても適していると思いました。
この記事がどなたかの参考になれば幸いです。